#!/usr/bin/python

"""
File:       nlu.py

Class:      CMPS 140 Winter 2011
Project:    Papa Bot
Date:       01/27/11
Authors:    Jason Erickson
            Morgan Davis
            DJ Visbal
            Constantine Perepelitsa
            Jae Kim
            Huu Huynh
"""

import string
import nltk
import sqlite3  # change to use wrapper when it's written

GRAMMAR_FILE = "grammar.cfg"  # used by parser to chunk sentences
PHRASE_FILE = "phrases.dict"

class NLU(object):
    """Natural Language Understanding

    The NLU class provides tools for turning text from the user into
    structured information that can be passed to the Dialog Manager.

    It can be used to break a sentence into its parts of speach, and
    to chunk a sentence using the grammar provided in the file 'grammar',
    located in the same directory as this file.

    To test in the python interpreter:
    1) run the interpreter in the directory with this file and grammar file
    2) >>> from nlu import NLU  # import this class
    3) >>> n = NLU()  # instantiate an NLU class
    4) >>> print n.pos('Your sentence here.')  # get parts of speech
    5) >>> print n.chunk('Your sentence here.') # chunk sentence

    Attributes:
        grammar: the grammar used for chunking sentences
        cp: chunk parser to break sentences into phrases and such
        drink_lookup: map drink name => drink id
        ingredient_lookup: map ingredient name => ingredient id
        category_lookup: map category => category id
        phrase_lookup: map user phrases => tag
        actions: list of actions the user can perform
        users: list of all registered users
        keywords: list of keywords (1 or more words) the user might say
        tagger: used for tagging user input
    """

    def __init__(self,debug_mode=False,db=None):
        """Initiate grammar, taggar, chunker, and lookups."""
        self.debug_mode = debug_mode
        self.db = db
        self.grammar = self.read_grammar()
        self.cp = nltk.RegexpParser(self.grammar)
        self.drink_lookup = self.build_drink_lookup()
        self.ingredient_lookup = self.build_ingredient_lookup()
        self.category_lookup = self.build_category_lookup()
        self.phrase_lookup = self.build_phrase_lookup()
        self.user_actions = self.get_user_actions ()
        #self.users = self.get_users()  # all registered usernames
        # -- should be UPDATED when one is added
        self.keywords = self.drink_lookup.keys() \
                        + self.ingredient_lookup.keys() \
                        + self.category_lookup.keys() \
                        + self.phrase_lookup.keys()
        self.keywords.sort(key=len, reverse=True)
        self.tagger = self.build_tagger()

    def build_drink_lookup(self):
        nameIDs = self.db.getAllDrinks()
        return dict([(name.lower(), id) for (id,name) in nameIDs])
    
    def build_ingredient_lookup(self):
        nameIDs = self.db.getAllIngredients()
        return dict([(name.lower(), id) for (id,name) in nameIDs])

    def build_category_lookup(self):
           """Returns a category => ID mapping."""
           nameIDs = self.db.getAllCategories()
           return dict([(name.lower(), id) for (id,name) in nameIDs])
    
    def build_phrase_lookup(self):
        f = open(PHRASE_FILE,'r')
        dict = eval(f.read())
        return dict

    def read_grammar(self):
        """Read the file GRAMMAR_FILE and return it's full text."""
        f = open(GRAMMAR_FILE, "r")
        grammar = f.read()
        f.close()
        return grammar
    
    def get_user_actions(self):
        """List of possible user actions."""
        return ["get_recipe",
                "list_drinks",
                "list_drinks_by_properties",
                "list_drinks_by_category",
                "list_drinks_from_cabinet",
                "list_cabinet",
                "recommend_drinks",
                "cabinet_add",
                "cabinet_remove",
                "kill_me"]

    def build_tagger(self):
        """Build "unigram" tagger specifically for drinks and
        ingredients which backs off to the standard NLTK tagger."""
        tag_model = dict(zip(self.ingredient_lookup.keys(),
                             ['INGREDIENT'] * len(self.ingredient_lookup))
                         + zip(self.drink_lookup.keys(),
                               ['DRINK'] * len(self.drink_lookup))
                         + zip(self.category_lookup.keys(),
                              ['CATEGORY'] * len(self.category_lookup)))
        tag_model.update(self.phrase_lookup) 
        # morgan: i'm removing this... it doesn't seem to be used?
        #tag_model.update(zip(self.users, ['USER']*len(self.users)))
        tagger = nltk.UnigramTagger(
            model=tag_model,
            backoff=nltk.data.load(nltk.tag._POS_TAGGER)
                                   )
        return tagger

    def pos(self, text):
        """Break a sentence into parts of speach, return as list of tuples."""
        # Many ingredients do not get tagged properly, and it's
        # difficult to extract this information after potentially
        # improper chunking.
        # Begin with a scan for ingredients first, tokenize them
        # specially, and then back off to a normal English POS tagging.
        tokens = self.findkeywords(text)
        return self.tagger.tag(tokens)

    def findkeywords(self, text):
        """Recursive method for finding all drink / ingredient mentions."""
        for keyword in self.keywords:
            if keyword not in text: continue
            # might be an off by one error.
            # do you want at most three splits or two splits?
            t = text.split(keyword, 2)
            return self.findkeywords(t[0]) + [keyword] \
                   + self.findkeywords(t[1])
        return nltk.word_tokenize(text)
		
    def traverse(self, tree, frame):
        """Recursive function to look for properties
           in a tree, which are then added to the frame."""
        try: # tree is a tree node (if try works)
            if tree.node in self.user_actions:
                frame.action = tree.node # found action the user wants to take
            elif tree.node == "PROPERTIE": # found drink propertie
                frame.drink_properties.append(tree[0][1])
            for child in tree:
                self.traverse(child, frame) # traverse children
        except: # tree is a tuple(word,tag) inside tree
            if tree[1] == "DRINK": # found drink name
                frame.drink = string.capwords(tree[0])
            elif tree[1] == "CATEGORY": # found drink category
                frame.category = string.lower(tree[0])
            elif tree[1] == "INGREDIENT": # found ingredient
                frame.ingredients.append(string.capwords(tree[0]))

    def chunk(self, text):
        """Break a sentence into chunks, return as a tree."""
        tag_text = self.pos(text)
        return self.cp.parse(tag_text)
    
    def get_name(self):
        """User's name isn't recognized; ask for it."""
        return raw_input().strip()
    
    def parse_user_response(self, text=None):
        """Transition function. Calls new_parse_user_response(), if that
           dosn't get any useful info, call old_parse_user_response()."""
        if text is None:
            text = raw_input().lower()
        parse_tree = self.chunk(text)
        if self.debug_mode: print parse_tree
        response = self.new_parse_user_response(parse_tree) # try advanced parser
        if response.action == None: # found useful info?
            response = self.fallback_parse_user_response(parse_tree) # fallback parser
        if self.debug_mode:
            print "response.answer:", response.answer
            print "response.action:", response.action
            print "response.drink:", response.drink
            print "response.ingredients:", repr(response.ingredients)
            print "response.category:", response.category
            print "response.drink_properties:", repr(response.drink_properties)
        return response

    def new_parse_user_response(self, parse_tree):
        """Turns user text into a structured frame using chunking."""
        response = UserResponseFrame() # frame
        self.traverse(parse_tree, response) # fill frame with tree
        return response # return frame
        
    def fallback_parse_user_response(self, parse_tree):
        """Turns user text into a structured frame"""
        if self.debug_mode: print "(*falling back to old parser*)"
        action_tags = ["GET_RECIPE", "WITH", "MANAGE_CABINET", "QUIT"]
        to_action = {"GET_RECIPE": "get_recipe",
                     "WITH": "list_drinks",
                     "QUIT": "quit" }
        response = UserResponseFrame() # frame
        leaves = parse_tree.leaves() # each leaf is a word or phrase
        # search the parse tree's leaves for key phrases
        for leaf in leaves:
            if leaf[1] in action_tags: # check for action key phrases
                if leaf[1] == "MANAGE_CABINET": # cabinet action
                    if leaf[0] == "add":
                        response.action = "cabinet_add"
                    elif leaf[0] == "remove":
                        response.action = "cabinet_remove"
                    else:
                        response.action = "list_cabinet"
                else:
                    response.answer = leaf[0]
                    response.action = to_action[leaf[1]]
            elif leaf[1] == "YES": # check for answer yes
                response.answer = "y"
            elif leaf[1] == "NO": # check for answer no
                response.answer = "n"
            elif leaf[1] == "DRINK": # check for drink name
                response.drink = string.capwords(leaf[0])
            elif leaf[1] == "INGREDIENT":
                response.ingredients.append(string.capwords(leaf[0]))
            elif leaf[1] == "CATEGORY":
                response.category = string.lower(leaf[0])
            elif leaf[1] == "USER":
                response.answer = leaf[0]
            elif leaf[1] == "CABINET":
                cabinet = True
        if response.category:
            response.action = "list_drinks_by_category"
        return response

class UserResponseFrame(object):
    """User Response Frame
    
    The User Response Frame represents the users response to a question 
    in a structured form. The NLU fills the frame with information taken 
    from the user text, such as if the user is saying a negative or 
    positive utterances, what action the user wants to take, any drinks 
    the user mentions, and any other possible information from the user.

    Attributes:
        action: action the user wants to perform
        drink: drink mentioned by user
        category: drink category mentioned my the user
        ingredients: list of ingredients mentioned by user
        drink_properties: eg. new, popular, strong
        answer: 'y' = yes, 'n' = no
    """
    def __init__(self):
        self.action = None
        self.drink = None
        self.category = None
        self.answer = None
        self.ingredients = []
        self.drink_properties = []

# for testing
if __name__ == "__main__":
    n = NLU();
    print "use q to quit"
    while (1):
        text = raw_input("Sentence: ").lower()
        if (text == "q"):
            break
        print n.chunk(text)
